//
// Written by Motti Shaked
//
// motti@wincode.net
//
// Version: 1.0.0.0
//
// DISCLAIMER:
//
// You may use this code as you please.
// You may modify and share it freely.
// You may NOT hold me liable for any damage
// caused to you, your company, your neighbors,
// or anyone else as a result of using this code,
// taking ideas from it, dreaming of it,
// or any other use you may find it suitable for.
// Whatever you do with this code is at 
// your own risk.
//
// Enjoy!
// 

using System;
using System.Runtime.Remoting.Channels;
using System.Collections;
using System.Reflection;
using System.Runtime.Remoting.Messaging;
using System.Threading;
using Universe.Remoting.Sinks;

namespace Universe.Remoting.Sinks
{
    public class CustomClientSinkProvider : BaseCustomSinkProvider, IClientChannelSinkProvider
    {
        private delegate BaseCustomSink CustomSinkCreator(IChannelSender channel, string url, object remoteChannelData);

        private IClientChannelSinkProvider nextProvider;

        // the following field is a delegate to methods that creates the custom sink
        // initially this will be CreateCustomSink which tries
        // both options of constructors
        // after it succeeded once, it will change this
        // delegate to point to the method that creates
        // using the right constructor.
        // after all, the custom sink type has a certain
        // constructor and it's not going to change in runtime, 
        // this way we can save time in future constructions.
        // also, if the client sink signals that it should be excluded
        // permanently, this delegate will point to a method that always returns null.
        // this optimization is not applied in the CustomServerSinkProvider
        // because the server custom sink is created once when the
        // channel is created, while a client sink is usually created multiple times
        // (when a proxy is created).
        // I must keep this delegate reference as an object, rather than as a
        // CustomSinkCreator, because I change its value using Interlocked.Exchange
        // (this actually hurts performance, because of the costly downcast).
        private object sinkCreator;

        public CustomClientSinkProvider(IDictionary properties, ICollection providerData) 
            : base(properties, providerData)
        {
            // direct the custom sink creator delegate to the general
            // creator method (see comments in the sinkCreator declaration).
            this.sinkCreator = new CustomSinkCreator(this.CreateCustomSink);
        }

        public CustomClientSinkProvider(Type customClientSink)
            : this(CreateProperties(customClientSink), new ArrayList())
        {
        }

        public IClientChannelSinkProvider Next
        {
            get {return this.nextProvider; }
            set {this.nextProvider = value;}
        }

        public IClientChannelSink CreateSink(IChannelSender channel, string url, object remoteChannelData) 
        {
            BaseCustomSink customSinkObject;

            // the custom sink must be provided the perProviderState object.
            // passing this object after construction is too late, because the sink
            // may need it in its constructor.
            // however, I did not want to affect the signature of every
            // possible sink constructor (and wanted to
            // allow writing basic custom sinks without explicitly writing a constructor).
            // so I pass it through the CallContext.
            CallContext.SetData("perProviderState", this.perProviderState);

            // create next sink in the chain
            IClientChannelSink next = this.nextProvider.CreateSink(channel, url, remoteChannelData);

            // create custom sink
            customSinkObject = ((CustomSinkCreator)this.sinkCreator)(channel, url, remoteChannelData);

            if (customSinkObject != null)
            {
                customSinkObject.SetNextSink(next);
                return customSinkObject;
            }
            else
            {
                return next;
            }
        }

        private BaseCustomSink CreateCustomSink(IChannelSender channel, string url, object remoteChannelData)
        {
            BaseCustomSink customSinkObject;
            CustomSinkCreator nullCreator = new CustomSinkCreator(NullCustomSink);

            // first construction option
            try
            {
                customSinkObject = CreateCustomSink1(channel, url, remoteChannelData);
                if (nullCreator != (CustomSinkCreator)this.sinkCreator)
                {
                    Interlocked.Exchange(ref this.sinkCreator, 
                                         new CustomSinkCreator(CreateCustomSink1));
                }
                return customSinkObject;
            }
            catch (System.Exception e)
            {
                // if the exception specifies that the target constructor 
                // was not found, this is ok.
                // go on to the next option.
                if (!(e.InnerException is MissingMethodException))
                {
                    throw;
                }
            }

            // second (and last) construction option
            //
            // not catching any exceptions here because this is
            // our last option for creating the custom sink.
            // if this failed - pass the exception to the caller.
            customSinkObject = CreateCustomSink2(channel, url, remoteChannelData);
            if (nullCreator != (CustomSinkCreator)this.sinkCreator)
            {
                Interlocked.Exchange(ref this.sinkCreator, 
                                     new CustomSinkCreator(CreateCustomSink2));
            }
            return customSinkObject;

        }

        private BaseCustomSink CreateCustomSink1(IChannelSender channel, string url, object remoteChannelData)
        {
            BaseCustomSink customSinkObject = null;

            // try to instantiate using a constructor that takes ClientSinkCreationData (or SinkCreationData)
            object [] par = {new ClientSinkCreationData(this.data, channel, url, remoteChannelData)};
            try
            {
                customSinkObject = (BaseCustomSink)Activator.CreateInstance(this.customSinkType, par);
            }
            catch (System.Exception e)
            {
                ExamineConstructionException(e);
            }

            return customSinkObject;
        }

        private BaseCustomSink CreateCustomSink2(IChannelSender channel, string url, object remoteChannelData)
        {
            BaseCustomSink customSinkObject = null;

            // try to instantiate using a parameterless constructor
            try
            {
                customSinkObject = (BaseCustomSink)Activator.CreateInstance(this.customSinkType);
            }
            catch (System.Exception e)
            {
                ExamineConstructionException(e);
            }

            return customSinkObject;
        }

        // the sinkCreator delegate will point to this method if the 
        // derived class asked to be permanently excluded from the sink chain
        private BaseCustomSink NullCustomSink(IChannelSender channel, string url, object remoteChannelData)
        {
            return null;
        }

        // this method has to examine the exception thrown from
        // a derived class' constructor and act accordingly
        private void ExamineConstructionException(System.Exception e)
        {
            if (e.InnerException is ExcludeMeException)
            {
                ExcludeMeException excludeExeption = (ExcludeMeException)e.InnerException;
                if (excludeExeption.ExcludeMePermanently)
                {
                    Interlocked.Exchange(ref this.sinkCreator, 
                                         new CustomSinkCreator(NullCustomSink));
                }
            }
            else
            {
                if (e is TargetInvocationException)
                {
                    throw new CustomSinkException(
                        string.Format("Could not create instance of {0}. {1} was thrown during construction. Message: {2}", 
                                      this.customSinkType.ToString(), e.InnerException.GetType().ToString(),
                                      e.InnerException.Message), e);
                }
                else
                {
                    throw new CustomSinkException(
                        string.Format("Could not create instance of {0}.", 
                                      this.customSinkType.ToString()), e);
                }
            }
        }
    }
}